Skip to content

Fix test broker leaks, state races, and signal-masked command failures#541

Open
apedroheringer wants to merge 1 commit into
openai:mainfrom
apedroheringer:fix/broker-teardown-state-locking
Open

Fix test broker leaks, state races, and signal-masked command failures#541
apedroheringer wants to merge 1 commit into
openai:mainfrom
apedroheringer:fix/broker-teardown-state-locking

Conversation

@apedroheringer

Copy link
Copy Markdown

Problem

Running npm test leaves dozens of orphaned processes behind (broker app-server-broker.mjs serve instances plus fake codex app-server children spawned from /tmp/codex-plugin-test-* workspaces). On one machine a single suite run left ~30 brokers + ~29 fake app-servers running after the temp dirs had already been cleaned up. Root causes, verified against main (db52e28):

  1. Test teardown is per-test and easy to miss. Most tests that lazily start a broker never run the SessionEnd hook (e.g. the lazy-broker status test at tests/runtime.test.mjs:2209, while its neighbours at :2153/:2198 do clean up). Any test failure or early return also skips cleanup.
  2. state.json writes are a read-modify-write with no lock and no atomic rename (lib/state.mjssaveState/updateState use a bare fs.writeFileSync). Two concurrent commands can lose updates or tear the file.
  3. ensureBrokerSession is check-then-create with no serialization (lib/broker-lifecycle.mjs). Two concurrent callers can both miss the existing broker and spawn duplicates, orphaning one.
  4. Signal-terminated commands are reported as success: lib/process.mjs coalesces spawnSync's status: null to 0, so runCommandChecked treats a SIGTERM/SIGKILL death as exit 0.

Changes

  • tests/helpers.mjs: makeTempDir now tracks created workspaces, and a global after() hook shuts down any broker a test left running (graceful broker/shutdown, then terminateProcessTree escalation), removes its session dir, and fails the suite loudly if a broker survives teardown — so leaks cannot reappear silently.
  • tests/runtime.test.mjs: the lazy-broker status test now runs SessionEnd like its neighbours.
  • lib/locking.mjs (new): minimal mkdir-based advisory lock with stale reclaim; sync and async variants; no new dependencies.
  • lib/state.mjs: saveState/updateState are serialized per workspace and write via temp file + fsync + atomic rename.
  • lib/broker-lifecycle.mjs: the check-then-create sequence in ensureBrokerSession is serialized per workspace.
  • lib/process.mjs: status: null with a signal is normalized to a non-zero status; runCommandChecked now reports signal=SIGTERM failures.

Testing

  • npm test: 95/95 passing (91 existing + 4 new regression tests: signal-terminated command reported as failure ×2, concurrent upsertJob from two processes loses no updates, concurrent ensureBrokerSession calls share a single broker).
  • Verified post-suite process table twice: zero leftover brokers / fake app-servers / temp session dirs.
  • npm run check-version passes.

Related work

🤖 Generated with Claude Code

https://claude.ai/code/session_01K9WP1dAojgJe3xXJMV9DM1

- tests/helpers.mjs: track temp workspaces and add a global after() teardown
  that shuts down any broker a test left running (graceful shutdown, then
  terminateProcessTree escalation) and removes its session dir; the suite
  now fails loudly if a broker survives teardown.
- tests/runtime.test.mjs: the lazy-broker status test now runs the
  SessionEnd hook like its neighbors instead of leaking its broker.
- lib/locking.mjs (new): minimal mkdir-based advisory lock with stale
  reclaim, sync and async variants.
- lib/state.mjs: serialize saveState/updateState per workspace and write
  state.json via temp file + fsync + atomic rename, so concurrent
  read-modify-write cycles no longer lose updates or tear the file.
- lib/broker-lifecycle.mjs: serialize ensureBrokerSession's check-then-create
  sequence per workspace so concurrent callers cannot spawn duplicate brokers.
- lib/process.mjs: stop coalescing a null spawnSync status to 0 when the
  process died from a signal; runCommandChecked now reports it as a failure.
- New regression tests: signal-terminated commands, concurrent upsertJob
  from two processes, concurrent ensureBrokerSession sharing one broker.
@apedroheringer
apedroheringer requested a review from a team July 22, 2026 21:52

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 878c86314f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread tests/helpers.mjs
Comment on lines +74 to +75
if (pidAlive(pid)) {
throw new Error(`Leaked broker process ${pid} survived suite teardown.`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not fail teardown on defunct broker PIDs

In environments where the detached broker has exited but remains as a zombie under PID 1, process.kill(pid, 0) still succeeds, so this new teardown path reports a leak that cannot be terminated. I hit this with npm test: all tests passed, then this hook failed with Leaked broker process ..., and ps showed that PID as [MainThread] <defunct>. That makes the suite fail nondeterministically depending on how quickly the container init reaps orphaned detached brokers; the leak check needs to distinguish defunct processes from live brokers before throwing.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant